/* *
 *
 *  Networkgraph series
 *
 *  (c) 2010-2019 Paweł Fus
 *
 *  License: www.highcharts.com/license
 *
 *  !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
 *
 * */
"use strict";
import H from "../../parts/Globals.js";
/* eslint-disable no-invalid-this, valid-jsdoc */
H.networkgraphIntegrations = {
  verlet: {
    /**
     * Attractive force funtion. Can be replaced by API's
     * `layoutAlgorithm.attractiveForce`
     *
     * @private
     * @param {number} d current distance between two nodes
     * @param {number} k expected distance between two nodes
     * @return {number} force
     */
    attractiveForceFunction: function (d, k) {
      // Used in API:
      return (k - d) / d;
    },
    /**
     * Repulsive force funtion. Can be replaced by API's
     * `layoutAlgorithm.repulsiveForce`
     *
     * @private
     * @param {number} d current distance between two nodes
     * @param {number} k expected distance between two nodes
     * @return {number} force
     */
    repulsiveForceFunction: function (d, k) {
      // Used in API:
      return ((k - d) / d) * (k > d ? 1 : 0); // Force only for close nodes
    },
    /**
     * Barycenter force. Calculate and applys barycenter forces on the
     * nodes. Making them closer to the center of their barycenter point.
     *
     * In Verlet integration, force is applied on a node immidatelly to it's
     * `plotX` and `plotY` position.
     *
     * @private
     * @return {void}
     */
    barycenter: function () {
      var gravitationalConstant = this.options.gravitationalConstant,
        xFactor = this.barycenter.xFactor,
        yFactor = this.barycenter.yFactor;
      // To consider:
      xFactor =
        (xFactor - (this.box.left + this.box.width) / 2) *
        gravitationalConstant;
      yFactor =
        (yFactor - (this.box.top + this.box.height) / 2) *
        gravitationalConstant;
      this.nodes.forEach(function (node) {
        if (!node.fixedPosition) {
          node.plotX -= xFactor / node.mass / node.degree;
          node.plotY -= yFactor / node.mass / node.degree;
        }
      });
    },
    /**
     * Repulsive force.
     *
     * In Verlet integration, force is applied on a node immidatelly to it's
     * `plotX` and `plotY` position.
     *
     * @private
     * @param {Highcharts.Point} node
     *        Node that should be translated by force.
     * @param {number} force
     *        Force calcualated in `repulsiveForceFunction`
     * @param {Highcharts.PositionObject} distance
     *        Distance between two nodes e.g. `{x, y}`
     * @return {void}
     */
    repulsive: function (node, force, distanceXY) {
      var factor = (force * this.diffTemperature) / node.mass / node.degree;
      if (!node.fixedPosition) {
        node.plotX += distanceXY.x * factor;
        node.plotY += distanceXY.y * factor;
      }
    },
    /**
     * Attractive force.
     *
     * In Verlet integration, force is applied on a node immidatelly to it's
     * `plotX` and `plotY` position.
     *
     * @private
     * @param {Highcharts.Point} link
     *        Link that connects two nodes
     * @param {number} force
     *        Force calcualated in `repulsiveForceFunction`
     * @param {Highcharts.PositionObject} distance
     *        Distance between two nodes e.g. `{x, y}`
     * @return {void}
     */
    attractive: function (link, force, distanceXY) {
      var massFactor = link.getMass(),
        translatedX = -distanceXY.x * force * this.diffTemperature,
        translatedY = -distanceXY.y * force * this.diffTemperature;
      if (!link.fromNode.fixedPosition) {
        link.fromNode.plotX -=
          (translatedX * massFactor.fromNode) / link.fromNode.degree;
        link.fromNode.plotY -=
          (translatedY * massFactor.fromNode) / link.fromNode.degree;
      }
      if (!link.toNode.fixedPosition) {
        link.toNode.plotX +=
          (translatedX * massFactor.toNode) / link.toNode.degree;
        link.toNode.plotY +=
          (translatedY * massFactor.toNode) / link.toNode.degree;
      }
    },
    /**
     * Integration method.
     *
     * In Verlet integration, forces are applied on node immidatelly to it's
     * `plotX` and `plotY` position.
     *
     * Verlet without velocity:
     *
     *    x(n+1) = 2 * x(n) - x(n-1) + A(T) * deltaT ^ 2
     *
     * where:
     *     - x(n+1) - new position
     *     - x(n) - current position
     *     - x(n-1) - previous position
     *
     * Assuming A(t) = 0 (no acceleration) and (deltaT = 1) we get:
     *
     *     x(n+1) = x(n) + (x(n) - x(n-1))
     *
     * where:
     *     - (x(n) - x(n-1)) - position change
     *
     * TO DO:
     * Consider Verlet with velocity to support additional
     * forces. Or even Time-Corrected Verlet by Jonathan
     * "lonesock" Dummer
     *
     * @private
     * @param {Highcharts.NetworkgraphLayout} layout layout object
     * @param {Highcharts.Point} node node that should be translated
     * @return {void}
     */
    integrate: function (layout, node) {
      var friction = -layout.options.friction,
        maxSpeed = layout.options.maxSpeed,
        prevX = node.prevX,
        prevY = node.prevY,
        // Apply friciton:
        diffX = (node.plotX + node.dispX - prevX) * friction,
        diffY = (node.plotY + node.dispY - prevY) * friction,
        abs = Math.abs,
        signX = abs(diffX) / (diffX || 1), // need to deal with 0
        signY = abs(diffY) / (diffY || 1);
      // Apply max speed:
      diffX = signX * Math.min(maxSpeed, Math.abs(diffX));
      diffY = signY * Math.min(maxSpeed, Math.abs(diffY));
      // Store for the next iteration:
      node.prevX = node.plotX + node.dispX;
      node.prevY = node.plotY + node.dispY;
      // Update positions:
      node.plotX += diffX;
      node.plotY += diffY;
      node.temperature = layout.vectorLength({
        x: diffX,
        y: diffY,
      });
    },
    /**
     * Estiamte the best possible distance between two nodes, making graph
     * readable.
     *
     * @private
     * @param {Highcharts.NetworkgraphLayout} layout layout object
     * @return {number}
     */
    getK: function (layout) {
      return Math.pow(
        (layout.box.width * layout.box.height) / layout.nodes.length,
        0.5
      );
    },
  },
  euler: {
    /**
     * Attractive force funtion. Can be replaced by API's
     * `layoutAlgorithm.attractiveForce`
     *
     * Other forces that can be used:
     *
     * basic, not recommended:
     *    `function (d, k) { return d / k }`
     *
     * @private
     * @param {number} d current distance between two nodes
     * @param {number} k expected distance between two nodes
     * @return {number} force
     */
    attractiveForceFunction: function (d, k) {
      return (d * d) / k;
    },
    /**
     * Repulsive force funtion. Can be replaced by API's
     * `layoutAlgorithm.repulsiveForce`.
     *
     * Other forces that can be used:
     *
     * basic, not recommended:
     *    `function (d, k) { return k / d }`
     *
     * standard:
     *    `function (d, k) { return k * k / d }`
     *
     * grid-variant:
     *    `function (d, k) { return k * k / d * (2 * k - d > 0 ? 1 : 0) }`
     *
     * @private
     * @param {number} d current distance between two nodes
     * @param {number} k expected distance between two nodes
     * @return {number} force
     */
    repulsiveForceFunction: function (d, k) {
      return (k * k) / d;
    },
    /**
     * Barycenter force. Calculate and applys barycenter forces on the
     * nodes. Making them closer to the center of their barycenter point.
     *
     * In Euler integration, force is stored in a node, not changing it's
     * position. Later, in `integrate()` forces are applied on nodes.
     *
     * @private
     * @return {void}
     */
    barycenter: function () {
      var gravitationalConstant = this.options.gravitationalConstant,
        xFactor = this.barycenter.xFactor,
        yFactor = this.barycenter.yFactor;
      this.nodes.forEach(function (node) {
        if (!node.fixedPosition) {
          var degree = node.getDegree(),
            phi = degree * (1 + degree / 2);
          node.dispX +=
            ((xFactor - node.plotX) * gravitationalConstant * phi) /
            node.degree;
          node.dispY +=
            ((yFactor - node.plotY) * gravitationalConstant * phi) /
            node.degree;
        }
      });
    },
    /**
     * Repulsive force.
     *
     * @private
     * @param {Highcharts.Point} node
     *        Node that should be translated by force.
     * @param {number} force
     *        Force calcualated in `repulsiveForceFunction`
     * @param {Highcharts.PositionObject} distanceXY
     *        Distance between two nodes e.g. `{x, y}`
     * @return {void}
     */
    repulsive: function (node, force, distanceXY, distanceR) {
      node.dispX += ((distanceXY.x / distanceR) * force) / node.degree;
      node.dispY += ((distanceXY.y / distanceR) * force) / node.degree;
    },
    /**
     * Attractive force.
     *
     * In Euler integration, force is stored in a node, not changing it's
     * position. Later, in `integrate()` forces are applied on nodes.
     *
     * @private
     * @param {Highcharts.Point} link
     *        Link that connects two nodes
     * @param {number} force
     *        Force calcualated in `repulsiveForceFunction`
     * @param {Highcharts.PositionObject} distanceXY
     *        Distance between two nodes e.g. `{x, y}`
     * @param {number} distanceR
     * @return {void}
     */
    attractive: function (link, force, distanceXY, distanceR) {
      var massFactor = link.getMass(),
        translatedX = (distanceXY.x / distanceR) * force,
        translatedY = (distanceXY.y / distanceR) * force;
      if (!link.fromNode.fixedPosition) {
        link.fromNode.dispX -=
          (translatedX * massFactor.fromNode) / link.fromNode.degree;
        link.fromNode.dispY -=
          (translatedY * massFactor.fromNode) / link.fromNode.degree;
      }
      if (!link.toNode.fixedPosition) {
        link.toNode.dispX +=
          (translatedX * massFactor.toNode) / link.toNode.degree;
        link.toNode.dispY +=
          (translatedY * massFactor.toNode) / link.toNode.degree;
      }
    },
    /**
     * Integration method.
     *
     * In Euler integration, force were stored in a node, not changing it's
     * position. Now, in the integrator method, we apply changes.
     *
     * Euler:
     *
     * Basic form: `x(n+1) = x(n) + v(n)`
     *
     * With Rengoild-Fruchterman we get:
     * `x(n+1) = x(n) + v(n) / length(v(n)) * min(v(n), temperature(n))`
     * where:
     * - `x(n+1)`: next position
     * - `x(n)`: current position
     * - `v(n)`: velocity (comes from net force)
     * - `temperature(n)`: current temperature
     *
     * Known issues:
     * Oscillations when force vector has the same magnitude but opposite
     * direction in the next step. Potentially solved by decreasing force by
     * `v * (1 / node.degree)`
     *
     * Note:
     * Actually `min(v(n), temperature(n))` replaces simulated annealing.
     *
     * @private
     * @param {Highcharts.NetworkgraphLayout} layout
     *        Layout object
     * @param {Highcharts.Point} node
     *        Node that should be translated
     * @return {void}
     */
    integrate: function (layout, node) {
      var distanceR;
      node.dispX += node.dispX * layout.options.friction;
      node.dispY += node.dispY * layout.options.friction;
      distanceR = node.temperature = layout.vectorLength({
        x: node.dispX,
        y: node.dispY,
      });
      if (distanceR !== 0) {
        node.plotX +=
          (node.dispX / distanceR) *
          Math.min(Math.abs(node.dispX), layout.temperature);
        node.plotY +=
          (node.dispY / distanceR) *
          Math.min(Math.abs(node.dispY), layout.temperature);
      }
    },
    /**
     * Estiamte the best possible distance between two nodes, making graph
     * readable.
     *
     * @private
     * @param {object} layout layout object
     * @return {number}
     */
    getK: function (layout) {
      return Math.pow(
        (layout.box.width * layout.box.height) / layout.nodes.length,
        0.3
      );
    },
  },
};
